package warriorCombat;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.ListIterator;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;


/**
 * This class accepts incoming connections from ArenaClient and serves as a combat moderator between clients
 * 
 * @author Brett Hoyer
 */
@SuppressWarnings("serial")
public class ArenaServer extends JFrame
{
	private JTextArea status;
	private JLabel numWarriors;
	private int nextSerial;

	private ServerSocket server;
	final private int PORT = 8001;

	private ExecutorService runGame;

	private ArrayList<ArenaWarrior> combatants;
	private Random rand = new Random(); //Random numbers used to calculate HP lost in combat

	//Attack Options
	private final String ATTACK1 = "Rock";
	private final String ATTACK2 = "Paper";
	private final String ATTACK3 = "Scissors";

	/**
	 * Create a new Arena Server GUI and launch thread
	 */
	private ArenaServer()
	{
		super("Combat Arena Server");
		combatants = new ArrayList<ArenaWarrior>();
		runGame = Executors.newCachedThreadPool();
		nextSerial = 1;

		try
		{
			server = new ServerSocket(PORT);
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}

		buildServerDisplay();
		execute();
	}

	/**
	 * Creates and displays the GUI for the server
	 */
	private void buildServerDisplay()
	{
		JPanel pnl = new JPanel(new BorderLayout());
		status = new JTextArea();
		JScrollPane scrl = new JScrollPane(status);
		scrl.setPreferredSize(new Dimension(600, 250));
		pnl.add(scrl, BorderLayout.NORTH);
		JPanel clientInfo = new JPanel(new FlowLayout());
		clientInfo.add(new JLabel("No. of Warriors in Arena: "));
		numWarriors = new JLabel("0");
		clientInfo.add(numWarriors);
		pnl.add(clientInfo, BorderLayout.CENTER);
		JButton close = new JButton("Close");
		close.addActionListener(new ActionListener()
		{
			public void actionPerformed(ActionEvent e)
			{
				closeApplication();
			}
		});
		pnl.add(close, BorderLayout.SOUTH);
		this.add(pnl);
		this.pack();
		this.setVisible(true);

		//Set actions taken on close
		this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		this.addWindowListener(new WindowAdapter()
		{
			public void windowClosing(WindowEvent e)
			{
				closeApplication();
			}
		});
	}

	private void execute()
	{
		try
		{
			status.append("Server IP: " + InetAddress.getLocalHost().getHostAddress());
		}
		catch (UnknownHostException e)
		{
			e.printStackTrace();
		}
		status.append("\nThe Arena is ready");

		while(true)
		{
			try
			{
				ArenaWarrior incoming = new ArenaWarrior(server.accept());
				synchronized (this)
				{
					combatants.add(incoming);
					if(!combatants.isEmpty())
						runGame.execute(combatants.get(combatants.size()-1));
				}
			}
			catch (IOException e)
			{
				e.printStackTrace();
			}
		}
	}

	/**
	 * Removes a combatant from the arena
	 * @param combatant object to be removed
	 */
	private synchronized void removeCombatant(ArenaWarrior combatant)
	{
		combatants.remove(combatant);
	}

	/**
	 * Updates server display by adding the provided string
	 * @param str String to be displayed in GUI
	 */
	private void updateDisplay(final String str)
	{
		SwingUtilities.invokeLater(
				new Runnable()
				{
					public void run()
					{
						status.append(str);
					}
				});
	}

	/**
	 * if clients are still connected prevents program from closing until all clients disconnect
	 * writes library to text file and closes program 
	 */
	private void closeApplication()
	{
		while(Integer.parseInt(numWarriors.getText()) == 0)
		{
			System.exit(0);		
		}
		status.append("\nPlease wait until all warriors have left the arena.");
	}

	/**
	 * Increase the displayed number of warriors by 1
	 */
	private void increaseNumWarriors()
	{
		SwingUtilities.invokeLater(
				new Runnable()
				{
					public void run()
					{
						int num = Integer.parseInt(numWarriors.getText());
						numWarriors.setText(Integer.toString(++num));
					}
				});
	}

	/**
	 * Decrease the displayed number of warriors by 1
	 * 
	 */
	private void decreaseNumWarriors()
	{
		SwingUtilities.invokeLater(
				new Runnable()
				{
					public void run()
					{
						int num = Integer.parseInt(numWarriors.getText());
						numWarriors.setText(Integer.toString(--num));
					}
				});

	}

	/**
	 * Searches ArrayList combatants for an arena warrior with given serial number
	 * @param ser serial number of arena warrior
	 * @return null is arenaWarrior is not found
	 */
	private ArenaWarrior getCombatant(int ser)
	{
		ListIterator<ArenaWarrior> list = combatants.listIterator();
		while(list.hasNext())
		{
			ArenaWarrior current = list.next();
			if(current.getWarrior().getSerial() == ser)
				return current;
		}

		return null;
	}

	/**
	 * Creates a list of all warriors currently in the areana
	 * @return list of warriors
	 */
	private String getWarriorList()
	{
		String str = "";
		ListIterator<ArenaWarrior> list = combatants.listIterator();
		while(list.hasNext())
		{
			str += list.next().getWarrior().toStringFull();
		}
		return str;
	}

	/**
	 * Initiates combat between two warriors
	 * @param input String containing attacker, attack, and victim separated by semicolons
	 */
	private void initiateCombat(String input)
	{
		String delim = "[;]+";
		String[] atk = input.split(delim);
		ArenaWarrior attacker = getCombatant(Integer.parseInt(atk[0]));
		ArenaWarrior defender = getCombatant(Integer.parseInt(atk[3]));
		String attack = atk[2];

		if(!defender.setOpponent(attacker, null, attack))
			attacker.sendMessage("RESOLUTION;MESSAGE;Cannot Attack Warrior;This Warrior is already engaged in combat");
		else
		{
			attacker.setOpponent(defender, attack, null);
			defender.sendMessage("DEFEND;" + atk[1]);
		}
	}

	/**
	 * Resolves combat between two warriors
	 * @param defense defenders combat option
	 */
	private void resolveCombat(ArenaWarrior defender, String defense)
	{
		ArenaWarrior attacker = defender.getOpponent();
		String attack = defender.getDefense();

		int winner = 0; //1 if attacker wins, 2 if defender wins, 0 if tie

		if(attack.equals(ATTACK1))
		{
			if(defense.equals(ATTACK2))
				winner = 2;
			else if(defense.equals(ATTACK3))
				winner = 1;
		}
		else if(attack.equals(ATTACK2))
		{
			if(defense.equals(ATTACK3))
				winner = 2;
			else if(defense.equals(ATTACK1))
				winner = 1;
		}
		else if(attack.equals(ATTACK3))
		{
			if(defense.equals(ATTACK1))
				winner = 2;
			else if(defense.equals(ATTACK2))
				winner = 1;
		}

		int damage = rand.nextInt(5)+ 5;
		if(winner != 0)
			damage *= 2;

		switch (winner)
		{
		case 1:
			attacker.sendMessage("RESOLUTION;MESSAGE;You Win!!;Your " + attack + " beats " + defense + ", you dealt " + damage + " damage to " + defender.getWarrior().getName() + ".;");
			attacker.closeCombat(0);
			defender.sendMessage("RESOLUTION;MESSAGE;You Lose!!;" + attack + " beats your " + defense + ", you take " + damage + " damage from " + attacker.getWarrior().getName() + ".;");
			defender.closeCombat(damage);
			updateDisplay("\n" + attacker.getWarrior().getName() + " did " + damage + " to " + defender.getWarrior().getName() + ".");
			break;
		case 2:
			defender.sendMessage("RESOLUTION;MESSAGE;You Win!!;Your " + defense + " beats " + attack + ", you dealt " + damage + " damage to " + attacker.getWarrior().getName() + ".;");
			defender.closeCombat(0);
			attacker.sendMessage("RESOLUTION;MESSAGE;You Lose!!;" + defense + " beats your " + attack + ", you take " + damage + " damage from " + defender.getWarrior().getName() + ".;");
			attacker.closeCombat(damage);
			updateDisplay("\n" + defender.getWarrior().getName() + " did " + damage + " damage to " + attacker.getWarrior().getName() + ".");
			break;
		default:
			attacker.sendMessage("RESOLUTION;MESSAGE;Tie;You and " + defender.getWarrior().getName() + " both used " + attack + ", you both take " + damage + " damage.;");
			attacker.closeCombat(damage);
			defender.sendMessage("RESOLUTION;MESSAGE;Tie;You and " + attacker.getWarrior().getName() + " both used " + attack + ", you both take " + damage + " damage.;");
			defender.closeCombat(damage);
			updateDisplay("\n" + attacker.getWarrior().getName() + " and " + defender.getWarrior().getName() + " each took " + damage + "damage.");
		}

		refreshAllClientOpponents();

	}

	/**
	 * Calls the refreshClientOpponents() method for all warriors
	 */
	private void refreshAllClientOpponents()
	{
		ListIterator<ArenaWarrior> list = combatants.listIterator();
		while(list.hasNext())
		{
			list.next().refreshClientOpponents();
		}
	}

	/**
	 * main method
	 * Creates a new Arena Server
	 */
	public static void main(String[] args)
	{
		new ArenaServer();
	}

	private class ArenaWarrior implements Runnable
	{
		private Socket connection;
		private BufferedReader in;
		private PrintWriter out;

		private Warrior warrior;

		private ArenaWarrior opponent = null;
		private String defense = null; //combat used against this warrior

		/**
		 * Constructor
		 * Set up new Client thread
		 * @param socket
		 */
		private ArenaWarrior(Socket socket)
		{
			connection = socket;

			try
			{
				in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
				out = new PrintWriter(connection.getOutputStream(), true);
				warrior = new Warrior(in.readLine(), nextSerial++);
				sendMessage(warrior.toStringFull());
			}
			catch (IOException e)
			{
				e.printStackTrace();
			}
		}

		/**
		 * Sends a message to the client
		 * @param msg sent to client
		 */
		private void sendMessage(String msg)
		{
			out.println(msg);
		}

		/**
		 * Sets an opponent for this warrior
		 * @param opponent opponent of warrior
		 * @param attack Combat used by this warrior
		 * @param defense Combat used against this warrior
		 * @return false if warrior already engaged in combat
		 */
		private boolean setOpponent(ArenaWarrior opponent, String attack, String defense)
		{
			if(this.opponent != null)
				return false;
			this.opponent = opponent;
			this.defense = defense;
			return true;			
		}

		/**
		 * Returns combat move used against this warrior
		 * @return defense
		 */
		private String getDefense()
		{
			return defense;
		}

		/**
		 * Returns this clients current opponent
		 * @return current opponent
		 */
		private ArenaWarrior getOpponent()
		{
			return opponent;
		}

		/**
		 * Returns this client's warrior
		 * @return warrior
		 */
		private Warrior getWarrior()
		{
			return warrior;
		}

		/**
		 * resets
		 */
		private void closeCombat(int damage)
		{
			warrior.loseHealth(damage);
			updateClientWarrior();

			opponent = null;
			defense = null;
		}

		/**
		 * Sends an updated list of opponents to client
		 */
		private void refreshClientOpponents()
		{
			sendMessage("REFRESH;" + getWarriorList());
		}

		/**
		 * Sends an updated warrior to the client
		 */
		private void updateClientWarrior()
		{
			sendMessage("UPDATE;" + warrior.toStringFull());
		}

		/**
		 * Parses information from client based on trigger words
		 * @param lineIn string received from client
		 */
		private void processRequest(String lineIn)
		{
			String delims = "[;]+";
			String[] input = lineIn.split(delims, 2);
			if(input[0].equals("REFRESH"))
				refreshClientOpponents();
			if(input[0].equals("ATTACK"))
				initiateCombat(input[1]);
			if(input[0].equals("DEFENSE"))
				resolveCombat(this, input[1]);
		}

		/**
		 * Executable method. controls communication between server and client
		 */
		public void run()
		{
			updateDisplay("\n" + warrior.getName() + " entered the arena.");
			increaseNumWarriors();

			refreshAllClientOpponents();

			try
			{
				String lineIn;
				while((lineIn = in.readLine())!= null)
				{
					if(lineIn.equals("CLOSE_CLIENT"))
						break;
					else
						processRequest(lineIn);
				}
			}
			catch (IOException e)
			{
				e.printStackTrace();
			}
			finally
			{
				try
				{
					updateDisplay("\n" + warrior.getName() + " has left the arena.");
					decreaseNumWarriors();
					out.close();
					in.close();
					connection.close();
					removeCombatant(this);
					refreshAllClientOpponents();
				}
				catch (IOException e)
				{
					e.printStackTrace();
				}
			}
		}
	}
}
